package com.tianlh.service.impl.redis.ReentrantLock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;

import redis.clients.jedis.Jedis;

/**
 * @author 黑子
 * @Date 2018/1/11 16:37
 */
public class RedisReentrantLock extends AbstractLock {

    private static final String DEFAULT_LOCK_KEY = "lock.lock";
    private static final long DEFAULT_LOCK_EXPIRES = 3000L;

    private Jedis jedis;

    protected String lockKey;

    // 锁的有效时长(毫秒)
    protected long lockExpires;

    public RedisReentrantLock(Jedis jedis) {
        this(jedis, DEFAULT_LOCK_KEY, DEFAULT_LOCK_EXPIRES);
    }

    public RedisReentrantLock(Jedis jedis, String lockKey, long lockExpires) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.lockExpires = lockExpires;
    }

    // 阻塞式获取锁的实现
    @Override
    protected boolean lock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt)
        throws InterruptedException {
        if (interrupt) {
            checkInterruption();
        }

        long start = localTimeMillis();
        long timeout = unit.toMillis(time);

        while (useTimeout ? !isTimeout(start, timeout) : true) {
            if (interrupt) {
                checkInterruption();
            }
            // 锁超时时间
            long lockExpireTime = serverTimeMillis() + lockExpires + 1;
            String newLockInfoJson = LockInfo.newForCurrThread(lockExpireTime).toString();
            // 条件能成立的唯一情况就是redis中lockKey还未关联value
            if (jedis.setnx(lockKey, newLockInfoJson) == 1) {
                //成功获取到锁, 设置相关标识
                locked = true;
                return true;
            }

            // value已有值, 但不能说明锁被持有, 因为锁可能expired了
            String currLockInfoJson = jedis.get(lockKey);
            if (currLockInfoJson == null) {
                continue;
            }

            LockInfo currLockInfo = LockInfo.fromString(currLockInfoJson);
            // 竞争条件只可能出现在锁超时的情况, 因为如果没有超时, 线程发现锁并不是被自己持有, 线程就不会去动value
            if (isTimeExpired(currLockInfo.getExpires())) {
                // 锁超时了
                LockInfo oldLockInfo = LockInfo.fromString(jedis.getSet(lockKey, newLockInfoJson));
                if (oldLockInfo != null && isTimeExpired(oldLockInfo.getExpires())) {
                    // 成功获取到锁, 设置相关标识
                    locked = true;
                    return true;
                }
            } else {
                // 锁未超时, 不会有竞争情况, 当前线程持有
                if (isHeldByCurrentThread(currLockInfo)) {
                    // 成功获取到锁, 设置相关标识,设置新的锁超时时间
                    currLockInfo.setExpires(serverTimeMillis() + lockExpires + 1);
                    currLockInfo.incCount();
                    jedis.set(lockKey, currLockInfo.toString());
                    locked = true;
                    return true;
                }
            }
        }
        locked = false;
        return false;
    }

    @Override
    public boolean tryLock() {
        long lockExpireTime = serverTimeMillis() + lockExpires + 1;
        String newLockInfo = LockInfo.newForCurrThread(lockExpireTime).toString();

        if (jedis.setnx(lockKey, newLockInfo) == 1) {
            locked = true;
            return true;
        }

        String currLockInfoJson = jedis.get(lockKey);
        if (currLockInfoJson == null) {
            // 再一次尝试获取
            if (jedis.setnx(lockKey, newLockInfo) == 1) {
                locked = true;
                return true;
            } else {
                locked = false;
                return false;
            }
        }

        LockInfo currLockInfo = LockInfo.fromString(currLockInfoJson);

        if (isTimeExpired(currLockInfo.getExpires())) {
            LockInfo oldLockInfo = LockInfo.fromString(jedis.getSet(lockKey, newLockInfo));
            if (oldLockInfo != null && isTimeExpired(oldLockInfo.getExpires())) {
                locked = true;
                return true;
            }
        } else {
            if (isHeldByCurrentThread(currLockInfo)) {
                currLockInfo.setExpires(serverTimeMillis() + lockExpires + 1);
                currLockInfo.incCount();
                jedis.set(lockKey, currLockInfo.toString());
                locked = true;
                return true;
            }
        }
        locked = false;
        return false;
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    @Override
    protected void unlock0() {

        // 1. 若锁过期, return
        // 2. 判断自己是否是锁的owner
        // 2.1 是, 若 count = 1, 则删除lockKey; 若 count > 1, 则计数减 1, return
        // 2.2 否, 则抛异常 IllegalMonitorStateException, reutrn
        // done, return
        LockInfo currLockInfo = LockInfo.fromString(jedis.get(lockKey));
        if (isTimeExpired(currLockInfo.getExpires())) {
            return;
        }

        if (isHeldByCurrentThread(currLockInfo)) {
            if (currLockInfo.getCount() == 1) {
                jedis.del(lockKey);
            } else {
                // 持有锁计数减1
                currLockInfo.decCount();
                String json = currLockInfo.toString();
                jedis.set(lockKey, json);
            }
        } else {
            throw new IllegalMonitorStateException(
                String.format("current thread[%s] does not holds the lock", Thread.currentThread().toString()));
        }

    }

    public boolean isLocked() {

        if (!locked) {
            return false;
        }
        String json = jedis.get(lockKey);
        if (json == null) {
            return false;
        }
        if (isTimeExpired(LockInfo.fromString(json).getExpires())) {
            return false;
        }
        return true;
    }

    public void release() {
        jedis.close();
    }

    // ------------------- utility methods ------------------------

    private boolean isHeldByCurrentThread(LockInfo lockInfo) {
        return lockInfo.isCurrentThread();
    }

    private void checkInterruption() throws InterruptedException {
        if (Thread.currentThread().isInterrupted()) {
            throw new InterruptedException();
        }
    }

    private boolean isTimeExpired(long time) {
        return time < serverTimeMillis();
    }

    private boolean isTimeout(long start, long timeout) {
        return start + timeout < System.currentTimeMillis();
    }

    private long serverTimeMillis() {
        return Long.parseLong(jedis.time().get(0));
    }

    private long localTimeMillis() {
        return System.currentTimeMillis();
    }
}
